iT邦幫忙

2024 iThome 鐵人賽

DAY 21
0

今天我們將在帳務報表頁面實作一個圓餅圖,使用 DGCharts 顯示每個物品的分類比例,讓我們能更方便地了解各分類在整體帳務中的比例。除了圓餅圖之外,還會顯示物品清單和總金額,提升報表功能的完整性。

目標

今天的實作目標主要是實現以下幾個功能:

  • 建立圓餅圖視覺化:我們將利用 DGCharts 來建立一個圓餅圖,並根據物品的分類金額比例顯示不同顏色。
  • 總金額顯示:圓餅圖的中心會顯示總金額。
  • 物品清單顯示:在圖表下方展示物品清單,顯示每個物品的名稱與價格。

DGCharts

DGCharts 是一個功能強大的圖表庫,專為 iOS 和 macOS 平台開發,基於 MPAndroidChart。它支援多種圖表類型,包括折線圖、柱狀圖、圓餅圖等,並且提供了豐富的自定義選項,讓開發者可以輕鬆地創建專業的資料視覺化圖表。在 SwiftUI 中,通過 UIViewRepresentable,我們可以將 DGCharts 與 SwiftUI 結合,從而實現更靈活的數據展示。DGCharts 是一個開源專案,在使用上具有極高的可擴展性和靈活性,非常適合用來處理動態資料和視覺化分析。

參考資料:

主要實作

實作 ReportViewModel

我們需要一個 ReportViewModel,它負責處理所有的資料抓取邏輯。

初始化

ReportViewModel 在初始化時會自動從資料庫中抓取資料,並且儲存到 items 中。

import DGCharts
import SwiftUI

class ReportViewModel: ObservableObject {
    @Published var items: [Item] = []
    @Published var dataEntries: [PieChartDataEntry] = []
    @Published var chartColors: [UIColor] = []
    let dataManager: DataManager

    init(dataManager: DataManager = DataManager()) {
        self.dataManager = dataManager
        fetchData()
    }
}

從資料庫中抓取資料

我們需要在 fetchData() 函數中撈取物品資料,並根據每個物品的分類進行統計。

func fetchData() {
    items = dataManager.fetchItems()
    generatePieChartData(items: items)
}

使用 dataManager.fetchItems() 來抓取所有的物品資料。接著,將這些資料傳遞給 generatePieChartData(),生成圓餅圖的資料。

生成圓餅圖的資料

接下來,我們需要根據每個物品的分類來生成對應的圓餅圖資料,並且給予每筆資料對應的顏色。

func generatePieChartData(items: [Item]) {
    if items.count > 0 {
        var categoryAmounts: [ItemCategory: Double] = [:]

        for item in items {
            let category = item.category
            categoryAmounts[category, default: 0] += item.price
        }

        dataEntries = categoryAmounts.map { (category, amount) in
            PieChartDataEntry(value: amount, label: category.name)
        }

        chartColors = categoryAmounts.map { category, _ in
            UIColor(hexString: category.categoryGroup.colorHex) 
        }
    } else {
        dataEntries = [PieChartDataEntry(value: 1)]
        chartColors = [UIColor(cgColor: CGColor(red: 80 / 255, green: 80 / 255, blue: 80 / 255, alpha: 1))]
    }
}

先用 categoryAmounts 來統計每個分類的金額,接著利用 PieChartDataEntry 生成對應的圓餅圖資料。最後,為每個分類設定一個顏色,而這個顏色來自於物品的大分類設定。

實作 ReportPieChartView

ReportPieChartView 是一個使用 UIViewRepresentable 的自定義 View,專門用來在 SwiftUI 中顯示 DGCharts 的圓餅圖。我們將使用 @Binding 將資料傳遞進來,當資料更新時,圓餅圖也能自動刷新。

初始化

我們先將 ReportPieChartView 初始化。在 makeUIView 中,我們設定了圓餅圖的基本樣式,包括是否顯示圖例、中心圓孔的大小,以及是否顯示資料的標籤。

import SwiftUI
import DGCharts

struct ReportPieChartView: UIViewRepresentable {
    @Binding var items: [Item]  // 綁定物品列表
    var entries: [PieChartDataEntry]  // 圓餅圖條目
    var colors: [UIColor]  // 顏色列表

    func makeUIView(context: Context) -> PieChartView {
        let chart = PieChartView()
        chart.holeRadiusPercent = 0.5  // 中心圓孔的大小
        chart.legend.enabled = false  // 隱藏圖例
        chart.centerText = "總金額"  // 圓心顯示的文字
        chart.drawEntryLabelsEnabled = false  // 隱藏資料標籤
        return chart
    }
}

更新 UIView 的內容

接下來要實作 updateUIView,這個函數會在資料變更時被呼叫。它會將我們從 ViewModel 中取得的資料和顏色更新到圓餅圖中。

func updateUIView(_ uiView: PieChartView, context: Context) {
    let dataSet = PieChartDataSet(entries: entries, label: "") // 生成圖表資料集
    dataSet.colors = colors  // 設定資料的顏色
    dataSet.selectionShift = 0 // // 點選後突出位置
    dataSet.drawValuesEnabled = false  // 不顯示資料的值

    let data = PieChartData(dataSet: dataSet)
    uiView.data = data

    // 更新中心的文字顯示
    let paragraphStyle = NSMutableParagraphStyle()
    paragraphStyle.alignment = .center
    let totalAmount = entries.reduce(0, { $0 + $1.value })  // 計算總金額
    let text = NSAttributedString(string: "總金額\n\(String(format: "$%.0f", totalAmount))", attributes: [
        .font: UIFont.boldSystemFont(ofSize: 30),
        .foregroundColor: UIColor.black,
        .paragraphStyle: paragraphStyle
    ])
    uiView.centerAttributedText = text
}

這段程式碼中,我們生成一個 PieChartDataSet,用來存放所有的資料,並設定每筆資料的顏色、字體等。接著,我們計算總金額,並將總金額顯示在圓餅圖的中心。

優化顯示效果

我們還需要處理一些細節來讓顯示效果更好,像是圓心文字的顯示,如果沒有物品,圓餅圖應顯示灰色並標示總金額為 0。

let text = NSAttributedString(string: "總金額\n\(String(format: "$%.0f", items.count > 0 ? totalAmount : 0))", attributes: [
    .font: UIFont.boldSystemFont(ofSize: 30),
    .foregroundColor: UIColor.black,
    .paragraphStyle: paragraphStyle
])

這樣即使沒有資料,也能顯示灰色的圓餅圖,並且在圓心顯示「總金額 0」。

實作 ReportView

最後我們來建立 ReportView,它會包含圓餅圖和物品列表,並透過 ReportViewModel 撈取資料。接著在畫面中呼叫我們剛剛寫好的 ReportPieChartView ,並在圓餅圖的下方顯示家用品列表,若資料庫沒有家用品紀錄,則顯示沒有紀錄。

import SwiftUI

struct ReportView: View {
    @ObservedObject var viewModel: ReportViewModel

    init(viewModel: ReportViewModel = ReportViewModel()) {
        self.viewModel = viewModel
    }
    
    var body: some View {
        VStack {
            ReportPieChartView(items: $viewModel.items, entries: viewModel.dataEntries, colors: viewModel.chartColors)
                .padding(10)
                .frame(height: 320)

            Spacer()
            if viewModel.items.count > 0 {
                List(viewModel.items) { item in
                    HStack {
                        Text(item.name)
                        Spacer()
                        Text("\(item.price, specifier: "%.2f")")
                    }
                }
            } else {
                VStack {
                    Image(systemName: "list.clipboard")
                        .resizable()
                        .frame(width: 100, height: 140)
                        .padding()
                    Text("沒有紀錄")
                }
                .foregroundColor(.gray)
                Spacer()
            }
        }
        .onAppear {
            viewModel.fetchData()
        }
        .navigationTitle("帳務報表")
    }
}

#Preview {
    ReportView(viewModel: ReportViewModel())
}

https://ooorito.com/wp-content/uploads/2024/09/%E7%B5%90%E6%9E%9C1.webp

https://ooorito.com/wp-content/uploads/2024/09/%E7%B5%90%E6%9E%9C2.webp

總結

今天我們成功將 DGCharts 整合到 SwiftUI 的 ReportView 中,並實現了圓餅圖的顯示。圓餅圖會根據物品的分類金額比例自動生成,並且中心顯示總金額。此外,我們也在圖表下方顯示了物品清單。今天先做到這邊,我們明天見!


上一篇
Day 20: SwiftUI 優化新增與編輯物品頁面
下一篇
Day 22: 更新帳務報表頁面 - 顯示分類比例與總金額
系列文
用 SwiftUI 掌控家庭日用品庫存30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言